"""
Core expect functions.

Copyright (c) 2009 Christopher Hesse
http://code.google.com/p/python-expect/
"""

import select
import time
import logging
from .patternhandler import PatternHandler, Continue
from .expectresult import ExpectResult
from .exceptions import Timeout, ChannelClosed
from .timer import Timer

log = logging.getLogger(__name__)

def is_sequence(obj):
    # returns True if the obj is a sequence (but not a string)
    return not isinstance(obj, str) and getattr(obj, '__iter__', False)

def expect(channels, patterns_or_handler, timeout):
    """
    Waits for the expectable to match any of the patterns or handler provided.
    
    The return value is an ExpectResult object containing all the information
    about the match, see the ExpectResult class for more information.
    """
    if not is_sequence(channels):
        channels = [channels]
        
    if isinstance(patterns_or_handler, PatternHandler):
        handler = patterns_or_handler
        return expect_handler(channels, handler, timeout)
    else:
        patterns = patterns_or_handler
    
        # pre-process patterns list
        
        # check if patterns is a dictionary
        if isinstance(patterns, dict):
            patterns = list(patterns.iteritems())
    
        # check if patterns is not a sequence, and make it one if it is
        if not is_sequence(patterns):
            patterns = [patterns]
            
        if len(channels) == 0:
            raise Exception("expect called with no channels")
    
        return expect_patterns(channels, patterns, timeout)
        
def expect_handler(channels, handler, timeout):
    """
    Expect using a PatternHandler, see the PatternHandler class for more
    information.
    """
    # copy the channels list
    remaining_channels = channels[:]
        
    timer = Timer(timeout)

    # check if any of the channels already match the patterns
    for channel in remaining_channels:
        result = match_patterns(channel, handler._patterns)
        if result:
            return_value = getattr(handler, result.name)(result, channel)
            # if we get a continue, we just keep going until we get something that is not a continue,
            # or else timeout
            if return_value is not Continue:
                return return_value
                        
    while True:
        # if the handler decided we should reset the timeout or change it, do so now
        if hasattr(handler, 'new_timeout'):
            timer.reset(handler.new_timeout)
            del handler.new_timeout
            
        if len(remaining_channels) == 0:
            log.debug("no channels remaining during expect operation")
            return None

        readable, _writable, _other = select.select(remaining_channels, [], [], timer.remaining)
        for channel in readable:
            if channel.closed:
                # channels are only closed after all remaining data has been read
                remaining_channels.remove(channel)
                handler.channel_closed(channel)
            else:
                # data available on this channel, read it and add it to the buffer
                data = channel.read()
                channel.read_buffer += data
                # now see if the buffer matches any of the patterns
                result = match_patterns(channel, handler._patterns)
                if result:
                    return_value = getattr(handler, result.name)(result, channel)
                    # if we get a continue, we just keep going until we get something that is not a continue,
                    # or else timeout
                    if return_value is not Continue:
                        return return_value
        
        if timer.remaining == 0:
            handler.timeout(remaining_channels)
    
def expect_patterns(channels, patterns, timeout):
    """
    Expect using a list of patterns. See the documentation on
    Expectable.expect() for more information.
    """
    # copy the channels list
    remaining_channels = channels[:]
    
    # check if timeout is in the patterns
    raise_error_on_timeout = True
    if Timeout in patterns:
        patterns.remove(Timeout)
        raise_error_on_timeout = False
        
    # check if channelclosed is in the patterns
    raise_error_on_closed = True
    if ChannelClosed in patterns:
        patterns.remove(ChannelClosed)
        raise_error_on_closed = False
        
    if timeout is None:
        timer = None
    else:
        timer = Timer(timeout)
    
    # check if any of the channels already match the patterns
    for channel in remaining_channels:
        result = match_patterns(channel, patterns)
        if result:
            return result
    
    while True:
        result = expect_loop(remaining_channels, patterns, timer)
        
        if result is ChannelClosed:
            if raise_error_on_closed:
                raise ChannelClosed("channel closed while expecting on channel", channel)
            else:
                return ChannelClosed
                
        elif result is Timeout:
            if raise_error_on_timeout:
                raise Timeout("timeout while waiting on channels", remaining_channels)
            else:
                return Timeout
                
        elif result is Continue:
            # carry on
            pass
        
        else:
            return result
        
def expect_loop(channels, patterns, timer):
    """
    Common expect loop for expect_handler and expect_patterns.
    """
    if timer is None:
        # wait forever
        readable, _writable, _other = select.select(channels, [], [])
    else:
        # wait for whatever the timer says we have left
        readable, _writable, _other = select.select(channels, [], [], timer.remaining)
    
    for channel in readable:
        if channel.closed:
            return ChannelClosed
        else:
            # data available on this channel, read it and add it to the buffer
            data = channel.read()
            channel.read_buffer += data
            # now see if the buffer matches any of the patterns, if it does, we're done
            result = match_patterns(channel, patterns)
            if result:
                return result
    
    if timer is not None and timer.remaining == 0:
        message = "timeout while waiting on {0} channels".format(len(remaining_channels))
        log.debug(message)
        return Timeout

    return Continue

def match_patterns(channel, patterns):
    """
    See if any patterns match the channel's buffer, return None if they don't.
    """
    # two pattern lists are supported
    # [pattern, ...]
    # [('name', pattern), ...]
    buffer = channel.read_buffer
    
    success = False
    for index, pattern in enumerate(patterns):
        # we currently support three pattern formats:
        # string, regular expression, and TextPattern
        if is_sequence(pattern):
            name, pattern = pattern
        else:
            name = index
        
        if isinstance(pattern, str):
            pos = buffer.find(pattern)
            if pos != -1:
                # found the pattern
                buffer_before = buffer[:pos]
                buffer_after = buffer[pos+len(pattern):]
                match = pattern
                match_object = match
                success = True
        else:
            # try regular expression or textpattern
            result = pattern.search(buffer)
            if result:
                buffer_before = buffer[:result.start()]
                buffer_after = buffer[result.end():]
                match = buffer[result.start():result.end()]
                match_object = result
                success = True
        
        if success:
            channel.read_buffer = buffer_after
            return ExpectResult(name=name, index=index, channel=channel, buffer_before=buffer_before,
                                match=match, buffer_after=buffer_after, match_object=match_object)
    return None